 // Copyright DustedPixels.com 2008. All rights reserved.

package com.dustedpixels.asm;

import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.analysis.Analyzer;
import org.objectweb.asm.tree.analysis.AnalyzerException;
import org.objectweb.asm.tree.analysis.Frame;
import org.objectweb.asm.tree.analysis.Value;

import com.dustedpixels.asm.Access;
import com.dustedpixels.asm.analysis.Analyzers;
import com.dustedpixels.asm.analysis.ThisInterpreter;


/**
 * @author micapolos@gmail.com (Michal Pociecha-Los)
 */
public final class ClassInliners {
  @SuppressWarnings("unchecked")
  public static ClassNode inlineComponentField(
      ClassNode containingClass, 
      FieldNode componentField, 
      ClassNode inlinedClass) {
    
    // Check that the field is final, private and non-static.
    if (!Access.isPrivate(componentField.access)) {
      throw new IllegalArgumentException("Field must be private");      
    }

    if (!Access.isFinal(componentField.access)) {
      throw new IllegalArgumentException("Field must be final");      
    }

    if (Access.isStatic(componentField.access)) {
      throw new IllegalArgumentException("Field must not be static");      
    }
    
    // Check that inlined class is top-level class (is not a subclass of another class).
    if (inlinedClass.superName == null) {
      throw new IllegalArgumentException("Can not inline java/lang/Object");
    } else if (!inlinedClass.superName.equals(Type.getType(Object.class).getInternalName())) {
      throw new IllegalArgumentException("Can not inline a class which extends another class");
    }
 
    // Copy source class.
    ClassNode resultClass = new ClassNode();
    containingClass.accept(resultClass);
    
    // Inline fields
    for (Object fieldObject : inlinedClass.fields) {
      FieldNode inlinedField = (FieldNode) fieldObject;
      inlinedField = processFieldForComponentInline(containingClass, componentField, inlinedClass, inlinedField);
      resultClass.fields.add(inlinedField);
    }
    
    // Inline all methods (including initializer methods)
    for (Object methodObject : inlinedClass.methods) {
      MethodNode inlinedMethod = (MethodNode) methodObject;
      inlinedMethod = processMethodForComponentInline(containingClass, componentField, inlinedClass, inlinedMethod);
      resultClass.methods.add(inlinedMethod);
    }
    
    return resultClass;
  }

  @SuppressWarnings("unchecked")
  public static MethodNode processMethodForComponentInline(
      ClassNode containingClass,
      FieldNode componentField,
      ClassNode inlinedClass,
      MethodNode inlinedMethod) {
    
    MethodNode processedMethod = new MethodNode(
        inlinedMethod.access | Opcodes.ACC_PRIVATE,
        MethodNodes.isConstructor(inlinedMethod) 
            ? componentField.name + "_init_"
            : componentField.name + "_" + inlinedMethod.name,
        inlinedMethod.desc,
        null,
        (String[]) inlinedMethod.exceptions.toArray(new String[0]));
          
    inlinedMethod.accept(processedMethod);
    
    // Detect references to "this"
    ThisInterpreter thisInterpreter = new ThisInterpreter();
    Analyzer thisAnalyzer = Analyzers.newUsing(thisInterpreter);
    Frame[] thisFrames;
    try {
      thisFrames = thisAnalyzer.analyze(inlinedClass.name, processedMethod);
    } catch (AnalyzerException e) {
      throw new RuntimeException(e);
    }

    AbstractInsnNode[] insns = processedMethod.instructions.toArray();
    
    for (int i = 0; i < insns.length; i++) {
      Frame thisFrame = thisFrames[i];
      AbstractInsnNode insn = insns[i];
      
      switch (insn.getOpcode()) {
        case Opcodes.GETFIELD: {
          // Field access of "this" object detected. Rename the field.
          Value thisValue = thisFrame.getStack(thisFrame.getStackSize() - 1);
          if (thisValue == ThisInterpreter.Values.THIS) {
            FieldInsnNode fieldInsn = (FieldInsnNode) insn;
            fieldInsn.name = componentField.name + "_" + fieldInsn.name;
            fieldInsn.owner = containingClass.name;
          }
          break;
        }
        
        case Opcodes.PUTFIELD: {
          // Field access of "this" object detected. Rename the field.
          Value thisValue = thisFrame.getStack(thisFrame.getStackSize() - 2);
          if (thisValue == ThisInterpreter.Values.THIS) {
            FieldInsnNode fieldInsn = (FieldInsnNode) insn;
            fieldInsn.name = componentField.name + "_" + fieldInsn.name;
            fieldInsn.owner = containingClass.name;
          }
          break;
        }
        
        case Opcodes.INVOKEINTERFACE:
        case Opcodes.INVOKEVIRTUAL:
        case Opcodes.INVOKESPECIAL: 
        case Opcodes.INVOKESTATIC: {
          // Method call on this detected. Rename the method.
          MethodInsnNode methodInsn = (MethodInsnNode) insn;
          if (insn.getOpcode() == Opcodes.INVOKESTATIC) {
            if (methodInsn.owner.equals(inlinedClass.name)) {
              methodInsn.name = componentField.name + "_" + methodInsn.name;
              methodInsn.owner = containingClass.name;
            }
          } else {
            int argCount = Type.getArgumentTypes(methodInsn.desc).length;
            Value thisValue = thisFrame.getStack(thisFrame.getStackSize() - argCount - 1);
            if (thisValue == ThisInterpreter.Values.THIS) {
              if (insn.getOpcode() == Opcodes.INVOKESPECIAL) {
                if (methodInsn.owner.equals(inlinedClass.name)) {
                  // We've got standard private method call (possibly call to other initializer)
                  methodInsn.name = methodInsn.name.equals("<init>")
                      ? componentField.name + "_init_"
                      : componentField.name + "_" + methodInsn.name;
                  methodInsn.owner = containingClass.name;
                } else {
                  if (methodInsn.owner.equals("java/lang/Object")) {
                    // We've got super(Object) method call, which we may actually ignore.
                    // TODO(micapolos): This call should probably be removed.
                  } else {
                    // We've got a call to priate method of another class, which is
                    // not supported
                    throw new IllegalArgumentException("Can not inline super calls");
                  }
                }
              } else {
                // We've got standard method call.
                methodInsn.name = componentField.name + "_" + methodInsn.name;
                methodInsn.owner = containingClass.name;
              }
            }
          }
          break;
        }
      }
    }
    
    return processedMethod; 
  }

  public static FieldNode processFieldForComponentInline(
      ClassNode containingClass,
      FieldNode componentField,
      ClassNode inlinedClass,
      FieldNode inlinedField) {
  
    return new FieldNode(
        inlinedField.access | Opcodes.ACC_PRIVATE,
        componentField.name + "_" + inlinedField.name,
        inlinedField.desc,
        null,
        inlinedField.value);
  }
}
